﻿using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.Maui.SourceGenerators.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Maui.SourceGenerators.Internal;

[Generator]
public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator
{
	static readonly SemanticValues emptySemanticValues = new(default, []);

	const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty";
	const string bindingModeFullName = "global::Microsoft.Maui.Controls.";

	const string bpAttribute = """
#nullable enable
namespace CommunityToolkit.Maui;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
sealed class BindablePropertyAttribute<TReturnType> : Attribute
{
	public string PropertyName { get; } = string.Empty;
	public Type? DeclaringType { get; set; }
	public object? DefaultValue { get; set; }
	public string DefaultBindingMode { get; set; } = string.Empty;
	public string ValidateValueMethodName { get; set; } = string.Empty;
	public string PropertyChangedMethodName { get; set; } = string.Empty;
	public string PropertyChangingMethodName { get; set; } = string.Empty;
	public string CoerceValueMethodName { get; set; } = string.Empty;
	public string DefaultValueCreatorMethodName { get; set; } = string.Empty;

	public BindablePropertyAttribute(string propertyName)
	{
		PropertyName = propertyName;
	}
}
""";

	public void Initialize(IncrementalGeneratorInitializationContext context)
	{
		context.RegisterPostInitializationOutput(ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8)));

		var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute`1",
			SyntaxPredicate, SemanticTransform)
			.Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty)
			.Collect()
			.SelectMany(static (types, _) => types);


		context.RegisterSourceOutput(provider, Execute);
	}

	void Execute(SourceProductionContext context, SemanticValues semanticValues)
	{
		var source = GenerateSource(semanticValues);
		SourceStringService.FormatText(ref source);
		context.AddSource($"{semanticValues.ClassInformation.ClassName}.g.cs", SourceText.From(source, Encoding.UTF8));
	}

	static string GenerateSource(SemanticValues value)
	{
		var sb = new StringBuilder($@"
// <auto-generated>
// Test2 : {DateTime.Now}

namespace {value.ClassInformation.ContainingNamespace};

{value.ClassInformation.DeclaredAccessibility} partial class {value.ClassInformation.ClassName}
{{
");

		foreach (var info in value.BindableProperties)
		{
			GenerateBindableProperty(sb, info);
			GenerateProperty(sb, info);
		}

		sb.AppendLine().Append('}');
		return sb.ToString();

		static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel info)
		{
			/*
			/// <summary>
			/// Backing BindableProperty for the <see cref="PropertyName"/> property.
			/// </summary>
			*/
			sb.AppendLine("/// <summary>")
				.AppendLine($"/// Backing BindableProperty for the <see cref=\"{info.PropertyName}\"/> property.")
				.AppendLine("/// </summary>");

			// public static readonly BindableProperty TextProperty = BindableProperty.Create(...);
			sb.AppendLine($"public static readonly {bpFullName} {info.PropertyName}Property = ")
							.Append($"{bpFullName}.Create(")
							.Append($"\"{info.PropertyName}\", ")
							.Append($"typeof({info.ReturnType}), ")
							.Append($"typeof({info.DeclaringType}), ")
							.Append($"{info.DefaultValue}, ")
							.Append($"{bindingModeFullName}{info.DefaultBindingMode}, ")
							.Append($"{info.ValidateValueMethodName}, ")
							.Append($"{info.PropertyChangedMethodName}, ")
							.Append($"{info.PropertyChangingMethodName}, ")
							.Append($"{info.CoerceValueMethodName}, ")
							.Append($"{info.DefaultValueCreatorMethodName}")
							.Append(");");

			sb.AppendLine().AppendLine();
		}

		static void GenerateProperty(StringBuilder sb, BindablePropertyModel info)
		{
			/*
			/// <inheritdoc />
			*/
			sb.AppendLine("/// <inheritdoc />");

			//public string Text
			//{
			//	get => (string)GetValue(TextProperty);
			//	set => SetValue(TextProperty, value);
			//}
			sb.AppendLine($"public {info.ReturnType} {info.PropertyName}")
							.AppendLine("{")
							.Append("get => (")
							.Append(info.ReturnType)
							.Append(")GetValue(")
							.AppendLine($"{info.PropertyName}Property);")
							.Append("set => SetValue(")
							.AppendLine($"{info.PropertyName}Property, value);")
							.AppendLine("}");
		}
	}

	static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
	{
		var classDeclarationSyntax = Unsafe.As<ClassDeclarationSyntax>(context.TargetNode);
		var semanticModel = context.SemanticModel;
		var classSymbol = (ITypeSymbol?)semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken);

		if (classSymbol is null)
		{
			return emptySemanticValues;
		}

		var classInfo = new ClassInformation(classSymbol.Name, classSymbol.DeclaredAccessibility.ToString().ToLower(), classSymbol.ContainingNamespace.ToDisplayString());


		var bindablePropertyModels = new List<BindablePropertyModel>(context.Attributes.Length);

		for (var index = 0; index < context.Attributes.Length; index++)
		{
			var attributeData = context.Attributes[index];
			bindablePropertyModels.Add(GetAttributeValues(attributeData, classSymbol?.ToString() ?? string.Empty));
		}

		return new(classInfo, bindablePropertyModels.ToImmutableArray());
	}

	static BindablePropertyModel GetAttributeValues(in AttributeData attributeData, in string declaringTypeString)
	{
		if (attributeData.AttributeClass is null)
		{
			throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass));
		}

		var bpType = attributeData.AttributeClass.TypeArguments[0];
		var defaultValue = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValue));
		var coerceValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName));
		var defaultBindingMode = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultBindingMode), "BindingMode.OneWay");
		var defaultValueCreatorMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName));
		var declaringType = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DeclaringType), declaringTypeString);
		var propertyChangedMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName));
		var propertyChangingMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName));
		var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString();
		var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));

		return new BindablePropertyModel
		{
			CoerceValueMethodName = coerceValueMethodName,
			DefaultBindingMode = defaultBindingMode,
			DefaultValue = defaultValue,
			DefaultValueCreatorMethodName = defaultValueCreatorMethodName,
			DeclaringType = declaringType,
			PropertyChangedMethodName = propertyChangedMethodName,
			PropertyChangingMethodName = propertyChangingMethodName,
			PropertyName = propertyName,
			ReturnType = bpType,
			ValidateValueMethodName = validateValueMethodName
		};
	}

	static bool SyntaxPredicate(SyntaxNode node, CancellationToken cancellationToken) =>
		node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
}